#include "Script.h"
#include "Helpers.h"

#include <sstream>

#ifdef __unix__
#include <sys/wait.h>
#endif

static bool system_failed(int rc) {
#ifdef _WIN32
  return (rc != 0); // Windows: system() return real exit code
#else
  if (rc == -1)
    return true; // system() can not run
  if (!WIFEXITED(rc))
    return true;               // stopped by signal
  return WEXITSTATUS(rc) != 0; // real return code
#endif
}

void Script::runCmdStr(std::string_view cmd) {
  std::cout << "Rum cmd: " << cmd << std::endl;

  int rc = std::system(cmd.data());

  if (system_failed(rc)) {
    fatalError("Command was failed:", cmd);
  }
}

static void runCmdDeviceLs(std::string_view deviceFilePath) { Script::runCmd("hdc shell ls -a ", deviceFilePath); }

static void runCmdWindowsLocalLs(std::string_view localFilePath) { Script::runCmd("dir ", localFilePath); }

static void runCmdLinuxLocalLs(std::string_view localFilePath) { Script::runCmd("ls ", localFilePath); }

static void runCmdLocalLs(std::string_view localFilePath) {
#ifdef _WIN32
  runCmdWindowsLocalLs(localFilePath);
#else
  runCmdLinuxLocalLs(localFilePath);
#endif
}

static void runCmdWindowsLocalRm(std::string_view localFilePath) {
  Script::runCmd("if exist ", localFilePath, " del /f ", localFilePath);
}

static void runCmdLinuxLocalRm(std::string_view localFilePath) { Script::runCmd("rm -f", localFilePath); }

static void runCmdLocalRm(std::string_view localFilePath) {
#ifdef _WIN32
  runCmdWindowsLocalRm(localFilePath);
#else
  runCmdLinuxLocalRm(localFilePath);
#endif
}

static std::string JoinPathLinux(std::string_view baseDir, std::string_view subDir) {
  std::string path(baseDir);
  path += '/';
  path += subDir;

  return path;
}

static std::string JoinPathWindows(std::string_view baseDir, std::string_view subDir) {
  std::string path(baseDir);
  path += '\\';
  path += subDir;

  return path;
}

static std::string JoinPath(std::string_view baseDir, std::string_view subDir) {
#ifdef _WIN32
  return JoinPathWindows(baseDir, subDir);
#else
  return JoinPathLinux(baseDir, subDir);
#endif
}

static void runCmdLocalLs(std::string_view localDirPath, std::string_view fileName) {
#ifdef _WIN32
  runCmdWindowsLocalLs(JoinPath(localDirPath, fileName));
#else
  runCmdLinuxLocalLs(JoinPath(localDirPath, fileName));
#endif
}

static void runCmdTargetMount() {
  Script::runCmd("hdc target mount");
  Script::runCmd("hdc shell setenforce 0");
}

static void runCmdReboot() { Script::runCmd("hdc shell reboot"); }

static void runCmdRemoveAOTStdLib() { Script::runCmd("hdc shell rm -fv /system/framework/etsstdlib_bootabc.an"); }

static void runCmdAOTStdLib() {
  runCmdTargetMount();
  runCmdRemoveAOTStdLib();
  Script::runCmd("hdc shell ark_aot --load-runtimes=ets --boot-panda-files=/system/framework/etsstdlib_bootabc.abc "
                 "--paoc-panda-files=/system/framework/etsstdlib_bootabc.abc "
                 "--paoc-output=/system/framework/etsstdlib_bootabc.an");
  runCmdDeviceLs("/system/framework/etsstdlib_bootabc.an");
}

static void runCmdRemoveAOTArkoala() { Script::runCmd("hdc shell rm -fv /system/framework/arkoala.an"); }

static void runCmdAOTArkoala() {
  runCmdTargetMount();
  runCmdRemoveAOTArkoala();
  Script::runCmd(
      "hdc shell ark_aot --load-runtimes=ets "
      "--boot-panda-files=/system/framework/etsstdlib_bootabc.abc:/system/framework/base_sdk.abc:/system/framework/"
      "resource.abc:/system/framework/arkoala.abc --paoc-panda-files=/system/framework/arkoala.abc "
      "--paoc-output=/system/framework/arkoala.an --compiler-enable-fast-interop=false");
  runCmdDeviceLs("/system/framework/arkoala.an");
}

static void runCmdRemoveAOTApp(std::string_view bundleName) { Script::runCmd("hdc shell bm compile -r ", bundleName); }

static void runCmdAOTApp(std::string_view bundleName) {
  runCmdTargetMount();
  runCmdRemoveAOTApp(bundleName);
  Script::runCmd("hdc shell bm compile -m full ", bundleName);
}

static void runCmdStartUserApp(std::string_view bundleName, std::string_view abilityName) {
  Script::runCmd("hdc shell aa start -b ", bundleName, " -a ", abilityName);
}

static void runCmdStopUserApp(std::string_view bundleName) { Script::runCmd("hdc shell aa force-stop ", bundleName); }

static void runCmdHiprofiler(std::string_view hiprofileConfigFilePath, std::string_view outputProfilePath,
                             std::string_view hiprofileTimeout) {
  Script::runCmd("hdc shell hiprofiler_cmd -c ", hiprofileConfigFilePath, " -o ", outputProfilePath, " -t ",
                 hiprofileTimeout, " -s -k");
}

static void runCmdRecvFile(std::string_view remoteDir, std::string_view localDir, std::string_view fileName) {
  Script::runCmd("hdc file recv ", remoteDir, "/", fileName, " ", localDir);
  runCmdLocalLs(localDir, fileName);
}

static void runCmdLockCpu() {
  runCmdTargetMount();
  constexpr int cpuCount = 12;
  for (int cpu = 0; cpu < cpuCount; ++cpu) {
    Script::runCmd("hdc shell \"echo 418000 > /sys/devices/system/cpu/cpu", cpu, "/cpufreq/scaling_max_freq\"");
  }
}

static void runCmdScpLibToDevice(std::string_view libName, std::string_view RemoteOutDir,
                                 std::string_view localTempDir) {
  std::unordered_map<std::string_view, std::pair<std::string_view, std::string_view>> libPathes = {
      {"libappspawn_ace.z.so",
       {"generic_generic_arm_64only/general_all_phone_standard/startup/appspawn", "/system/lib64/appspawn/appspawn"}},
      {"libets_environment.z.so",
       {"generic_generic_arm_64only/general_all_phone_standard/ability/ability_runtime", "/system/lib64/platformsdk"}},
  };

  auto it = libPathes.find(libName);
  if (it == libPathes.end()) {
    fatalError("Uknown libName:", libName);
  }

  runCmdTargetMount();

  std::string_view remoteSubDir = it->second.first;
  std::string_view devicePath = it->second.second;

  std::string localFilePath = JoinPath(localTempDir, libName);

  runCmdLocalRm(localFilePath);
  Script::runCmd("scp ", RemoteOutDir, "/", remoteSubDir, "/", libName, " ", localFilePath);
  runCmdLocalLs(localFilePath);

  Script::runCmd("hdc file send ", localFilePath, " ", JoinPathLinux(devicePath, libName));
}

std::string_view Script::getVar(std::string_view varName) const {
  auto it = variables_.find(std::string(varName));
  if (it == variables_.end()) {
    fatalError("Missed mandatory argument: ", varName);
    return "";
  }

  return it->second;
}

void Script::scriptSwitch(std::string_view cmd) {
  if (cmd == "TargetMount") {
    runCmdTargetMount();
  } else if (cmd == "Reboot") {
    runCmdReboot();
  } else if (cmd == "RemoveAOTStdLib") {
    runCmdRemoveAOTStdLib();
  } else if (cmd == "RemoveAOTArkoala") {
    runCmdRemoveAOTArkoala();
  } else if (cmd == "RemoveAOTApp") {
    runCmdRemoveAOTApp(getVar("bundleName"));
  } else if (cmd == "AOTStdLib") {
    runCmdAOTStdLib();
  } else if (cmd == "AOTArkoala") {
    runCmdAOTArkoala();
  } else if (cmd == "AOTApp") {
    runCmdAOTApp(getVar("bundleName"));
  } else if (cmd == "StartUserApp") {
    runCmdStartUserApp(getVar("bundleName"), getVar("abilityName"));
  } else if (cmd == "StopUserApp") {
    runCmdStopUserApp(getVar("bundleName"));
  } else if (cmd == "Hiprofiler") {
    cmdThreads_.emplace_back([this]() {
      std::ostringstream outputProfileGeneratedName;
      outputProfileGeneratedName << getVar("outputProfileName") << "_" << getDateTime() << ".htrace";

      std::ostringstream outputProfilePath;
      outputProfilePath << getVar("outputProfileDir") << "/" << outputProfileGeneratedName.str();

      runCmdHiprofiler(getVar("hiprofileConfigFilePath"), outputProfilePath.str(), getVar("hiprofileTimeout"));
      runCmdDeviceLs(outputProfilePath.str());
      runCmdRecvFile(getVar("outputProfileDir"), getVar("hiprofileLocalDir"), outputProfileGeneratedName.str());
      Script::runCmd("hdc shell rm -fv ", outputProfilePath.str());
    });

    // Wait for init hiprofiler.
    std::this_thread::sleep_for(std::chrono::milliseconds(3000));
  } else if (cmd == "LockCpu") {
    runCmdLockCpu();
  } else if (cmd == "ScpLibToDevice") {
    runCmdScpLibToDevice(getVar("libName"), getVar("RemoteOutDir"), getVar("localTempDir"));
  } else {
    fatalError("Trying call missing command:", cmd);
  }
}

Script::Script(std::string_view cmds, const std::unordered_map<std::string, std::string> &variables)
    : variables_(variables) {
  for (size_t cmdStPos = 0;;) {
    size_t cmdEndPos = cmds.find(',', cmdStPos);

    if (cmdEndPos == std::string::npos) {
      cmds_.emplace_back(cmds.substr(cmdStPos));
      break;
    }

    cmds_.emplace_back(cmds.substr(cmdStPos, cmdEndPos - cmdStPos));
    cmdStPos = cmdEndPos + 1;
  }
}

void Script::runScript() {
  int repeatCount = 1;

  auto it = variables_.find("repeatCount");
  if (it != variables_.end()) {
    repeatCount = std::stoi(it->second);
  }
  std::cout << "script will be repeated " << repeatCount << " times" << std::endl;

  for (int i = 0; i < repeatCount; ++i) {
    for (const auto &cmd : cmds_) {
      scriptSwitch(cmd);
    }

    JoinAndClearThreads();
  }
}

void Script::JoinAndClearThreads() {
  std::cout << "Waiting for " << cmdThreads_.size() << " threads" << std::endl;

  for (auto &t : cmdThreads_) {
    t.join();
  }
  cmdThreads_.clear();
}

Script::~Script() { JoinAndClearThreads(); }
